---
title: 死锁
description: 使用细粒度锁来锁定多个资源时，需要注意死锁的问题。
---

在[互斥锁](./互斥锁.mdx)一文中使用 Account.class 作为互斥锁来解决转账问题，虽然这个方案不存在并发问题，但是所有的操作都是串行化，存在严重的性能问题。

## 使用细粒度锁解决

其实涉及到两个资源，使用两把锁就搞定了 ，转出账户一把，转入账户一把。

![cb18e672732ab76fc61d60bdf66bf855](https://img.wkq.pub/pic/cb18e672732ab76fc61d60bdf66bf855.webp)



```java title='下面是具体的代码实现'
public class Account {
    private int balance;

    void transfer(Account target, int amt) {
        //锁定转出账户
        synchronized (this) { ①
            //锁定转入账户
            synchronized (target) { ②
                if (this.balance > amt) {  
                    this.balance -= amt;
                    target.balance += amt;
                }
            }
        }
    }
}
```

当两把锁都获取成功后，才执行转账操作。

上面的实现相比使用 Account.class 作为互斥锁，锁定的范围小了很多，这样的锁，叫**细粒度锁。使用细粒度锁可以提高并行度，是性能优化的一个重要手段。**

但是，**使用细粒度锁可能会导致死锁。**

死锁的一个比较专业定义是：**一组相互竞争资源的线程因互相等待，而导致永久阻塞的现象。**

![829d69c7d32c3ad1b89d89fc56017d1c](https://img.wkq.pub/pic/829d69c7d32c3ad1b89d89fc56017d1c.webp)

## 预防死锁

并发程序一旦死锁，一般没有好的办法，很多时候都只能重启应用。因此，解决死锁问题的最好方法是规避死锁。

死锁只有在以下四个条件都发生时才会出现死锁：

1. 互斥，共享资源  X 和 Y 只能被一个线程占用；
2. 占有且等待，线程 T1 已经取得共享资源 X ，在等待共享资源 Y 的时候，不释放共享资源 X;
3. 不可抢占，其他线程不能抢占线程 T1 占有的资源；
4. 循环等待，线程 T1 等待线程 T2 占有的资源，线程 T2 等待线程 T1 占有的资源，就是循环等待；

**只要破坏上述四个条件中的一个，就可以避免死锁的发生**。

其中，互斥这个条件没有办法破环，因为使用锁为的就是互斥。

1. 对于**占有且等待**，可以一次性申请全部资源；
2. 对于**不可抢占**，占有部分资源的线程进一步申请其它资源时，如果申请不到，可以主动释放它占有的资源；
3. 对于**循环等待**，可以通过按序申请资源来预防 ；

### 1. 破坏占有且等待条件

在并发编程领域，一次性申请这个操作是一个临界区，所以需要一个角色（）管理这个临界区，这里使用 Allocator 。它可以同时申请资源 apply() 和同时释放资源 free()。

```java title='Allocator.java'
package concurrent;

import java.util.ArrayList;
import java.util.List;

public class Allocator {
    private List<Object> als = new ArrayList<>();
    private static volatile Allocator instance;

    private Allocator() {
    }

    public static Allocator getInstance() {
        if (instance == null) {
            synchronized (Allocator.class) {
                if (instance == null) {
                    instance = new Allocator();
                }
            }
        }
        return instance;
    }

    //一次性申请所有资源
    synchronized boolean apply(Object from, Object to) {
        if (als.contains(from) || als.contains(to)) {
            return false;
        } else {
            als.add(from);
            als.add(to);
        }
        return true;
    }

    //释放资源
    synchronized void free(Object from, Object to) {
        als.remove(from);
        als.remove(to);
    }
}
```

```java title='Account.java'
package concurrent;

public class Account {
    private Allocator actr = Allocator.getInstance();
    private int balance;
    //一次性申请所有资源
    void transfer(Account target, int amt) {
        //一次性申请转出账户和转入账户，直到成功
        while (actr.apply(this, target)) ;
        try {
            synchronized (this) {
                synchronized (target) {
                    if (this.balance > amt) {
                        this.balance -= amt;
                        target.balance += amt;
                    }
                }
            }
        } finally {
            actr.free(this, target);
        }
    }
}
```

### 2. 破环不可抢占条件

破环不可抢占条件看上去简单，核心是要能够主动释放它占有的资源，但这一点是 synchronized 做不到的。

原因是 synchronized 申请资源的时候，如果申请不到，线程就直接进入阻塞状态，而线程进入阻塞状态，啥也干不了，也释放不了线程已经占有的资源。

Java 在语言层次确实没有解决这个问题，不过在 SDK 层面还是解决了的。java.util.concurrent 这个包下提供的 Lock 是可以轻松解决这个问题。

### 3. 破环循环等待条件

破环这个条件，需要对资源进行排序，然后按序申请资源。

```java
	package concurrent;

public class Account {
    private int balance;
    private int id;

    //一次性申请所有资源
    void transfer(Account target, int amt) {
        Account left = this;
        Account right = target;
        if (target.id < this.id) {
            left = target;
            right = this;
        }
        //锁定资源编号小的账户
        synchronized (left) {
            //锁定资源编号大的账户
            synchronized (right) {
                if (this.balance > amt) {
                    this.balance -= amt;
                    target.balance += amt;
                }
            }
        }
    }
}
```

## 总结



**使用细粒度锁来锁定多个资源时，要注意死锁的问题**



预防死锁主要是破环三个条件中的一个，在选择具体方案的时候，还需要评估以下操作成本，从中选择一个成本最低的方案。

